const vscode = require('vscode');
const { modify } = require('jsonc-parser');
const ejs = require('ejs');
const prototype = require('./prototype');

class PlatoGraphEditorProvider {
  constructor(context, categoryTree) {
    this.context = context;
    this.categoryTree = categoryTree;
  }

  getNonce() {
    let text = '';
    const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    for (let i = 0; i < 32; i++) {
      text += possible.charAt(Math.floor(Math.random() * possible.length));
    }
    return text;
  }

  updateDocument(edit, document, path, value) {
    const options = {
      formattingOptions: {
        insertSpaces: true,
        tabSize: 2,
        eol: '\n',
      },
    };
    modify(document.getText(), path, value, options).forEach((x) => {
      if (x.length === 0) {
        edit.insert(document.uri, document.positionAt(x.offset), x.content);
      } else if (x.content === '') {
        const start = document.positionAt(x.offset);
        const end = document.positionAt(x.offset + x.length);
        edit.delete(document.uri, new vscode.Range(start, end));
      } else {
        const start = document.positionAt(x.offset);
        const end = document.positionAt(x.offset + x.length);
        edit.replace(document.uri, new vscode.Range(start, end), x.content);
      }
    });
  }

  initDocument(document, json) {
    const edit = new vscode.WorkspaceEdit();
    edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), JSON.stringify(json, null, 2));
    vscode.workspace.applyEdit(edit);
  }

  validateDocument(document) {
    try {
      let json = JSON.parse(document.getText());
      let changed = false;
      if (!json.variables) {
        changed = true;
        json = { ...json, variables: [] };
      }
      if (!json.nodes) {
        changed = true;
        json = { ...json, nodes: [] };
      }
      if (!json.links) {
        changed = true;
        json = { ...json, links: [] };
      }
      if (!json.prototypes) {
        changed = true;
        json = { ...json, prototypes: [] };
      }
      if (changed) {
        this.initDocument(document, json);
      }
    } catch (err) {
      this.initDocument(document, {
        variables: [],
        nodes: [],
        links: [],
        prototypes: [],
      });
    }
  }

  resolveCustomTextEditor(document, webviewPanel) {
    this.validateDocument(document);

    const bundleUri = webviewPanel.webview.asWebviewUri(
      vscode.Uri.joinPath(this.context.extensionUri, 'media', 'bundle.js')
    );
    const codiconsUri = webviewPanel.webview.asWebviewUri(
      vscode.Uri.joinPath(this.context.extensionUri, 'node_modules', 'vscode-codicons', 'dist', 'codicon.css')
    );
    const codiconsFontUri = webviewPanel.webview.asWebviewUri(
      vscode.Uri.joinPath(this.context.extensionUri, 'node_modules', 'vscode-codicons', 'dist', 'codicon.ttf')
    );
    const nonce = this.getNonce();

    const updateGraph = () => {
      webviewPanel.webview.postMessage({
        type: 'UPDATE_GRAPH',
        text: document.getText(),
      });
    };
    const updateCategories = () => {
      webviewPanel.webview.postMessage({
        type: 'UPDATE_CATEGORIES',
        categories: this.categoryTree.categories,
      });
    };

    this.categoryTree.onDidChangeTreeData(() => {
      updateCategories();
    });

    webviewPanel.webview.onDidReceiveMessage((e) => {
      const edit = new vscode.WorkspaceEdit();

      switch (e.type) {
        case 'SHOW_ERROR':
          vscode.window.showErrorMessage(e.msg);
          break;

        case 'REPLACE_GRAPH':
          edit.replace(
            document.uri,
            new vscode.Range(0, 0, document.lineCount, 0),
            JSON.stringify(
              {
                variables: e.variables,
                nodes: e.nodes,
                links: e.links,
                prototypes: e.prototypes,
              },
              null,
              2
            )
          );
          vscode.workspace.applyEdit(edit);
          break;

        case 'ADD_NODE':
          this.updateDocument(edit, document, ['nodes', e.index], {
            id: e.id,
            category: e.category.title,
            title: e.node.title,
            pos: e.pos,
            values: e.node.inputs.map((pin) => prototype.defaultValue(pin.type)),
          });
          e.links.forEach((link) => {
            this.updateDocument(edit, document, ['links', link.index], {
              srcNodeId: link.srcNodeId,
              srcPinTitle: link.srcPinTitle,
              dstNodeId: link.dstNodeId,
              dstPinTitle: link.dstPinTitle,
            });
          });
          vscode.workspace.applyEdit(edit);
          break;

        case 'MOVE_NODES':
          e.nodes.forEach(({ index, pos }) => {
            this.updateDocument(edit, document, ['nodes', index, 'pos'], pos);
          });
          vscode.workspace.applyEdit(edit);
          break;

        case 'CHANGE_PIN_VALUE':
          this.updateDocument(edit, document, ['nodes', e.nodeIndex, 'values', e.pinIndex], e.value);
          vscode.workspace.applyEdit(edit);
          break;

        case 'ADD_VARIABLE':
          vscode.window.showInputBox({ placeHolder: 'Title', prompt: 'New Variable:' }).then((val) => {
            if (val) {
              this.updateDocument(edit, document, ['variables', e.index], {
                id: e.id,
                title: val,
                type: 'bool',
                default: false,
              });
              vscode.workspace.applyEdit(edit);
            }
          });
          break;

        case 'DELETE_VARIABLE':
          this.updateDocument(edit, document, ['variables', e.index], undefined);
          vscode.workspace.applyEdit(edit);
          break;

        case 'EDIT_VARIABLE_TITLE':
          vscode.window.showInputBox({ value: e.variable.title, prompt: 'Edit Variable Title:' }).then((val) => {
            if (val) {
              this.updateDocument(edit, document, ['variables', e.index, 'title'], val);
              vscode.workspace.applyEdit(edit);
            }
          });
          break;

        case 'EDIT_VARIABLE_TYPE':
          let type;
          let key;
          let value;

          vscode.window
            .showQuickPick(prototype.DATA_TYPES.concat(this.categoryTree.structs.map((x) => x.title)), {
              placeHolder: 'Type',
              canPickMany: false,
            })
            .then((val) => {
              if (val) {
                type = val;
                if (prototype.hasKey(type)) {
                  return vscode.window.showQuickPick(
                    prototype.SIMPLE_TYPES.concat(this.categoryTree.structs.map((x) => x.title)),
                    { placeHolder: 'Key', canPickMany: false }
                  );
                } else {
                  this.updateDocument(edit, document, ['variables', e.index], {
                    ...e.variable,
                    type,
                    key: null,
                    value: null,
                    default: prototype.defaultValue(type),
                  });
                  vscode.workspace.applyEdit(edit);
                }
              }
            })
            .then((val) => {
              if (val) {
                key = val;
                if (prototype.hasValue(type)) {
                  return vscode.window.showQuickPick(
                    prototype.SIMPLE_TYPES.concat(this.categoryTree.structs.map((x) => x.title)),
                    { placeHolder: 'Value', canPickMany: false }
                  );
                } else {
                  this.updateDocument(edit, document, ['variables', e.index], {
                    ...e.variable,
                    type,
                    key,
                    value: null,
                    default: prototype.defaultValue(type),
                  });
                  vscode.workspace.applyEdit(edit);
                }
              }
            })
            .then((val) => {
              if (val) {
                value = val;
                this.updateDocument(edit, document, ['variables', e.index], {
                  ...e.variable,
                  type,
                  key,
                  value,
                  default: prototype.defaultValue(type),
                });
                vscode.workspace.applyEdit(edit);
              }
            });
          break;

        case 'CHANGE_VARIABLE_DEFAULT':
          this.updateDocument(edit, document, ['variables', e.index, 'default'], e.value);
          vscode.workspace.applyEdit(edit);
          break;

        case 'ADD_VARIABLE_NODE':
          this.updateDocument(edit, document, ['nodes', e.nodeIndex], e.variable);
          if (e.prototype) {
            this.updateDocument(edit, document, ['prototypes', e.prototypeIndex], e.prototype);
          }
          vscode.workspace.applyEdit(edit);
          break;
      }
    });

    webviewPanel.webview.options = {
      enableScripts: true,
    };

    webviewPanel.onDidChangeViewState((e) => {
      if (e.webviewPanel.visible) {
        updateCategories();
        updateGraph();
      }
    });

    const onDidChangeTextDocument = vscode.workspace.onDidChangeTextDocument((e) => {
      if (e.document.uri.toString() === document.uri.toString()) {
        updateGraph();
      }
    });

    webviewPanel.onDidDispose(() => {
      onDidChangeTextDocument.dispose();
    });

    const scp = `default-src 'none'; img-src https:; font-src ${codiconsFontUri}; script-src 'unsafe-eval' ${webviewPanel.webview.cspSource}; style-src 'unsafe-inline' ${codiconsUri};`;
    ejs
      .renderFile(vscode.Uri.joinPath(this.context.extensionUri, 'media', 'index.ejs').fsPath, {
        scp,
        bundleUri,
        codiconsUri,
        nonce,
      })
      .then((html) => {
        webviewPanel.webview.html = html;
        updateCategories();
        updateGraph();
      });
  }
}

module.exports = PlatoGraphEditorProvider;
